iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 3
1
AI & Data

30 天學會深度學習和 Tensorflow系列 第 3

02. 鐵達尼預測內幕:資料和特徵處理

  • 分享至 

  • xImage
  •  

上一篇中,利用了 Tensorflow estimator API 建立了一個 Logistic Regression 分類器。但是相信大家還是一頭霧水,這個分類器到底為我們做了些什麼事情 。今天以及隨後陸續的幾篇文章,我們將對上一篇的原始碼做更詳盡的解說,以及了解進行一個機械學習的訓練該有的基礎觀念。

就像準備應試學生,拿著考古題訓練自己對該學科的理解能力一樣。準備考試的學生,通常會先將考古題分成兩部分:一部分可以藉由邊做題目邊檢查解答,以糾正自己理解錯誤的地方。另外一部分的試題,則會先將解答覆蓋起來,待做完所有這部分的試題後,再檢查解答,用此方法評估自己的學習成果。

相同地,機械學習演算法,在訓練時,也會將現有的訓練資料分成兩個部分:第一個部分被稱為 training set,使用於模型的訓練過程中。在 training set 中每一個訓練實例的正確分類標籤,都會被納入在訓練的過程裡。第二個部分則被稱為 validation set,主要是用來評估訓練的結果。在這個過程中,已訓練好的模型將不會對 validation set 繼續訓練,相對地,模型將會一一預測在 validation set 的實例,最後再以適合評估該訓練任務的 metrics 對 validation set 做模型評估(Model Evaluation)。

在分類任務中常用的 metrics 就是分類的正確率,又稱為 accuracy,主要是計算分類正確的比例。

Model evaluation 主要的功能是防止模型過度學習,就好比應試的學生,將考古題內的所有答案,死記起來,反而無法將學到的知識應用在尚未見過的題目裡。同樣地,我們也希望透過機械學習完成訓練的模型,有足夠的能力將學到的規則,推論到尚未見到的例子中,這在文獻中又被稱為模型 generalization 的能力。

如何將資料分成訓練和評估資料,則有很多不同的方法。其中一種最常用的被稱為 Cross-Validation。Cross-Validation 將所有的訓練資料,分成 K 等份,其中 K - 1 的訓練資料會在訓練模型時使用,也就是作為 training set。而留下的一份,則在評估中使用,也就是 validation set。如此循環 K 次以確保所有的資料都有機會選進 validation set。

5-fold validaiton illustration
圖一:簡單的表達 5-fold cross-validation 該如何進行。圖中的綠色部分為 validation set,而白色部分為 training set。

在過去資料不足的情況下,如鐵達尼的資料集,其資料量不到一千的筆數,會使用一種叫做 Jack-Knife 的方法,在這個方法中一次只用一個訓練例子,當作評估例子,循環訓練的次數則與訓練資料集的大小相等(在 scikit-learn 中則被稱為 Leave-One-Out)。然而現在的資料及數量大多充足,所以選定 K 為 5 或 10 通常都已足夠。K 的選擇,最終還是要看訓練集內資料的分佈程度。如果資料分布過於 diverse,則偏好較大的 K 值,以確保 training set 內的資料能捕捉原資料分布的 diversity。

為了說明方便,這裡採用訓練類神經網路常用的模型評估的方式,也就是將訓練資料單純的分成兩份:一份是 training set,另外一份是 evaluation set。訓練類神經網路較少使用 k-fold cross validation 的原因在於:訓練類神經網路通常相當耗時,所以 cross-validation 這樣的方法比較不適合評估類神經網路。

接下來就是特徵工程的部分。特徵工程包括了資料清洗,資料前置處理,和特徵選擇三個步驟。資料清洗,在英文中被稱為 data cleaning。在上篇中我們先對 ‘Age’ 這一欄位裡的遺失資料(missing value),填進一個任意數值就是進行資料清洗的工作。至於填入什麼數值,端看遺失資料的分佈情況,和該欄位對預測的重要性。因為 ‘Age ’這個欄位的數值遺失狀況,不是很嚴重,且鐵達尼資料的訓練數量非常少量,所以將遺失資料剔除,並不是很好的方法。相較而言,填入 0,這一個在年齡中,不可能出現的數值,有能代表資料上的缺失的情況。

以下程式碼,就是利用 pandas package 讀入 csv 格式的訓練資料後,將 'Age' 欄位遺失的部分填上零的數字。

import pandas as pd
from sklearn.model_seleciton import train_test_split

data_frame = pd.read_csv('../input/train.csv')
train_set, valid_set = train_test_split(data_frame.index, test_size=0.01)
data_frame.loc[:, 'Age'] = data_frame.loc[:, 'Age'].fillna(0)

因為有了兩個不同的資料集,現在我們需要兩個 input function,一個是給 LinearClassifier 物件呼叫 train 方法時使用(train_input),而另外一個則是呼叫 evaluate 方法使用(eval_input)。

from sklearn.preprocessing import MinMaxScaler
from functools import partial

def train_input(features, labels):
    source = tf.data.Dataset.from_tensor_slices((features, labels)) 
    return source.batch(10).repeat()

  
def eval_input(features, labels, text, vectorizer, selector):
    vectorized_ = vectorizer.transform(text)
    selected_ = selector.transform(vectorized_)
    features['PersonInfo'] = (selected_.toarray() + 1).astype(np.float32)
    source = tf.data.Dataset.from_tensors((features, labels)) 
    return source

其實是資料前置處理的工作,此工作包含了對特徵座標準化和重新編碼等。一般的特徵,可就其值,分為特徵是連續數值分佈,或有限且離散分佈。連續數值分佈的特徵如 'Fare',通常所進行的前處理是對數值做標準化,或壓縮原數值在有界的數值範圍間。標準化有時又被稱為正規化,在 tensorflow 的 feature_column 則藉由提供一個 normalization function (見 normalizer_fn 引數)來達成,可以見下面的原始碼範例:

from sklearn.preprocessing import MinMaxScaler

normalizer = MinMaxScaler()
normalizer.fit(data_frame.loc[train_set, ['Fare', 'Age']])
# numerical features
feature_columns = []
feature_columns.append(
    tf.feature_column.numeric_column('Fare', normalizer_fn=lambda x: x*normalizer.scale_[0]))
feature_columns.append(
    tf.feature_column.numeric_column('Age', normalizer_fn=lambda x: x*normalizer.scale_[1]))

標準化或正規化的方式,在往後提到梯度下降最佳化演算法的時候,才會對這一個部分做詳細介紹。

對於有限且離散分佈的特徵,在英文中又被稱為 categorical feature。他們的值有些是文字,如 “Sex” 欄位,所以必須提供編碼的字彙集(vocabulary_list)來將此欄位轉換成 Logistic regression 可以接受的矩陣型態。編碼的方式是用所謂的 one-hot encoding。在這個編碼方式下產生的矩陣,有著與字彙集大小相等的行數,矩陣每一行都代表一個在字彙集中的值。

one hot encoding

上圖簡單表示當 Sex 欄位用 one-hot encoding 後的表示。圖左方是 Sex 欄位的字彙集,右方則是相對應的 one-hot encoding 方式。可以看到,編碼矩陣的行數和字彙集大小相同,為二。每一個例子只能有一行元素其值為 1,而其所在行,代表該訓練例子的 Sex 欄位中是 FemaleMale

除了將每一個可能出現的值列舉出來外,做 one-hot encoding 外,另外一個方法則是根據數值的分佈情況,從原來的字彙集中選取代表性高的數個值,來建構新的字彙集。通常這一個方法適合原字彙集數量較大,或其分佈有過度集中在少數幾個值的情況。在 Parch (一等親同在船上的數量)和 SibSp (二等親同在船上的數量),就是這種情況。

histogram of Parch / SibSp

對於此種情況,我們可以根據值的分佈,建立新的 bucket,並對這些特徵重新分配新值。(見 bucketized_column 的 boundaries 引數和 categorical_column_with_identity 的 num_buckets 引數)

# categorical features
feature_columns.append(tf.feature_column.indicator_column(
    tf.feature_column.categorical_column_with_identity('Pclass', num_buckets=4)))
feature_columns.append(tf.feature_column.bucketized_column(
    source_column=tf.feature_column.numeric_column('SibSp'),
    boundaries=[1, 2, 4]))
feature_columns.append(tf.feature_column.bucketized_column(
    source_column=tf.feature_column.numeric_column('Parch'), 
    boundaries=[1, 2, 3]))
feature_columns.append(tf.feature_column.indicator_column(
    tf.feature_column.categorical_column_with_vocabulary_list(
    'Sex', vocabulary_list=['female', 'male'])))

而字彙集數量過大的情況,則多出現在原資料中文字的欄位裡,如 Name 欄位中,除了名字外還包括了稱謂。文字欄位的處理,會仰賴一種叫做 Bag of Word 的 vectorization 方法。在下面的原始碼中,我們利用 scikit-learn 的 CountVectorizer 來對 Name 和 Ticket 欄位來做 vectorization。

from sklearn.feature_extraction.text import CountVectorizer

desc = data_frame.loc[train_set, 'Name'].str.cat(
  data_frame.loc[train_set, 'Ticket'], sep=' ').values

vectorizer = CountVectorizer()
vectorized_ = vectorizer.fit_transform(desc)
print(len(vectorizer.vocabulary_))
# 2186

可以看到如果沒有做處理,直接 vectorization 的特徵向量長度,將會非常的大,所以我們就需要第三步驟 -- 特徵選取。 在這裡我們使用 scikit-learn 的 univariate 的方式來做特徵選取。因為 CountVectorizer 會傳回每一段字詞在句子裡的數目,所以在建立 feature_column 可以當作 numeric_column 來處理,同樣的我們也需要對 CountVectorizer 傳回的 sparse 矩陣,先轉為 dense array 再加上 1,對計數為零的元素做 Laplace smooth(加一)後再做正規化。

from sklearn.feature_selection import (GenericUnivariateSelect, mutual_info_classif)

selector = GenericUnivariateSelect(score_func=mutual_info_classif, mode='k_best', param=200)
select_ = selector.fit_transform(vectorized_, data_frame.loc[train_set, 'Survived'])
features['PersonInfo'] = (select_.toarray() + 1).astype(np.float32)
feature_columns.append(tf.feature_column.numeric_column(
  'PersonInfo', shape=(200,), normalizer_fn=lambda x: x/tf.reduce_sum(x)))

既然已經有了 validation set 來做模型的評估,我們現在再重新訓練一次模型,並輸出評估的預測正確率,來看看我們的模型 performance 如何?

features = dict(data_frame.loc[train_set, FEATURES])
labels = features.pop('Survived')

train_ds = train_input(features, labels)

learning_rate = 0.01
gd_optimizer = tf.train.GradientDescentOptimizer(learning_rate=learning_rate)
classifier = tf.estimator.LinearClassifier(
        optimizer=gd_optimizer, 
        feature_columns=feature_columns
        )
classifier = classifier.train(input_fn=partial(train_input, features, labels), 
                 max_steps=10)
test_features = dict(data_frame.loc[valid_set, FEATURES])
test_labels = test_features.pop('Survived')
test_text = data_frame.loc[valid_set, 'Name'].str.cat(data_frame.loc[valid_set, 'Ticket']).values
classifier.evaluate(input_fn=partial(eval_input, test_features, test_labels, test_text, vectorizer, selector)
)
# output
# {'accuracy': 0.6666667,
# 'accuracy_baseline': 0.6666666,
# 'auc': 0.5277778,
# 'auc_precision_recall': 0.5448412,
# 'average_loss': 0.61401296,
# 'label/mean': 0.33333334,
# 'loss': 5.526117,
# 'precision': 0.0,
# 'prediction/mean': 0.37537125,
# 'recall': 0.0,
# 'global_step': 10}

在下一篇中,我們將會對最佳化演算法做更詳細介紹。

圖片來源:

  1. 圖一來自 Learning Curves for Machine Learning

上一篇
01. 使用邏輯迴歸預測誰能在鐵達尼船難中存活?
下一篇
03. 鐵達尼預測內幕:梯度下降學習法
系列文
30 天學會深度學習和 Tensorflow30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言